// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Author: tu@tulrich.com (Thatcher Ulrich)

// Plotting utilities.


// datasets == list of datasets to plot
function Plot(datasets) {
  var me = this;
  me.datasets_ = datasets;
  me.mini_row_pitch_ = 5;
  me.rows_by_group_ = {};
  me.groups_ = [];
  map(datasets, function(k, dataset) {
      map(dataset.metric, function(metric_name, metric) {
          var group = me.groupName(metric_name);
          if (me.rows_by_group_[group] == undefined) {
            me.rows_by_group_[group] = [];
          }
          me.rows_by_group_[group].push(metric_name);
          me.groups_.push(group);
        });
    });
  map(me.rows_by_group_, function(k, v) {
      me.rows_by_group_[k] = array_dedupe(v).sort();
    });

  me.groups_ = array_dedupe(me.groups_).sort();
  me.tags_ = array_dedupe(map(datasets, function(k,v) { return v.tag; })).sort();

  me.datasets_by_tag_ = {};
  map(me.datasets_, function(k,v) { me.datasets_by_tag_[v.tag] = v; });
}

Plot.prototype.groupName = function(metric_name) {
  var first_slash = metric_name.indexOf('/');
  if (first_slash == -1) {
    return metric_name;
  } else {
    return metric_name.substring(0, first_slash + 1);
  }
};

Plot.prototype.paint = function(canvas, opts, rows) {
  var x = 0;
  var y = (opts.row_pitch - opts.data_height) / 2;
  var width = canvas.width;
  var ctx = canvas.getContext('2d');
  if (!ctx) {
    log('Plot.paint failed to get context');
    return;
  }

  var me = this;
  map(rows, function(k, row) {
  //  map(me.rows_, function(k,row) {
      // Find the min/max values to be displayed in this group of rows.
      var min = 1e30;
      var max = -1e30;
      map(me.tags_, function(k, tag) {
          var data = me.datasets_by_tag_[tag].metric[row];
          var minmax = me.getDataMinMax(data);
          min = Math.min(min, minmax[0]);
          max = Math.max(max, minmax[1]);
        });
      if (max == min) {
        // Guard against division-by-0.
        if (min > 1e-6) {
          min = 0;
        } else {
          max = min + 4;
        }
      }
      // Map the data range into the width of the canvas.
      var right_pad = opts.right_pad;
      var left_pad = opts.left_pad + opts.label_width;
      var datafunc = function(x) {
        return (x - min) / (max - min) * (opts.width - (right_pad + left_pad)) + left_pad;
      }

      if (opts.text_height && opts.label_width) {
        // Show the label.
        ctx.font = opts.text_height + 'px monospace';
        ctx.fillStyle = '#000';
        ctx.fillText(row, 0, y + opts.text_height / 2 + opts.data_height / 2);
      }

      var y_start = y;
      var overlay_self = false;
      map(me.tags_, function(k, tag) {
          // Draw a row.
          if (tag == 'self' && me.tags_.length > 1) {
            // Special treatment for the 'self' tag -- skip it now,
            // and finish it later by overlaying on top of the other
            // plots.
            overlay_self = true;
          } else {
            var data = me.datasets_by_tag_[tag].metric[row];
            if (opts.draw_tag_labels && opts.small_text_height) {
              ctx.font = opts.small_text_height + "px sans-serif";
              ctx.fillStyle = me.pickColor(k);
              ctx.fillText(tag, opts.label_width, y + opts.small_text_height / 2);
            }
            me.paintData(ctx, datafunc, width, y, data, k, opts);
            y += opts.row_pitch;
          }
        });
      if (overlay_self) {
        var data = me.datasets_by_tag_['self'].metric[row];
        if (typeof(data) == 'number') {
          var x = datafunc(data);
          ctx.strokeStyle = '#0F0';
          me.line(ctx, x, y_start, x, y);
	  ctx.strokeStyle = '#080';
	  // Fatten the self line.
	  me.line(ctx, x - 1, y_start, x - 1, y);
	  me.line(ctx, x + 1, y_start, x + 1, y);
        } else {
          log('self data type not number! ==> ', data);
        }
      }

      if (opts.axis_label_height) {
        var axis_y = y - opts.row_pitch + opts.data_height + 2.5;
        ctx.fillStyle = '#000';
        ctx.strokeStyle = '#000';
        me.line(ctx, datafunc(min), axis_y, datafunc(max), axis_y);
        me.labelAxis(ctx, datafunc, min, max, axis_y, opts);
        y += opts.axis_label_height;
      }
      
      if (me.tags_.length - overlay_self > 1) {
        // If more than one plot per row, then separate the rows with some space.
        y += opts.row_pitch;
      }
    });
};

Plot.prototype.formatNumber = function(x) {
  var suffix = [
      [1024 * 1024 * 1024, 'G'],
      [1024 * 1024, 'M'],
      [1024, 'K'],
             ];
  for (var i = 0; i < suffix.length; i++) {
    var div = x / suffix[i][0];
    if (div < 1) continue;
    if (div == Math.floor(div)) {
      var result = String(div) + suffix[i][1];
      return result;
    }
  }
  if (x >= 1000) {
    // Add commas.
    var num = String(x);
    var n = num.indexOf('.');
    if (n == -1) n = num.length;
    var out = num.substring(n);
    while (n > 0) {
      n -= 3;
      out = num.substring(n, n + 3) + out;
      if (n > 0) {
        out = "," + out;
      }
    }
    return out;
  }
  return String(x);
};

Plot.prototype.labelAxis = function(ctx, datafunc, min, max, axis_y, opts) {
  // Label the interval [min,max] with three or four nice round numbers.

  var range = max - min;

  var f = function(range, pow, div, mul) {
    return Math.pow(pow, Math.floor(Math.log(range / (div * mul)) / Math.log(pow))) * mul;
  };
  var powdivmul = [
      [ 10, 5, 1 ],
      [ 10, 5, 2 ],
      [ 10, 5, 5 ],
      [ 10, 5, 25 ],
      [  2, 5, 1 ],
      [  2, 2, 1 ],
                   ];
  var candidates = [];
  for (var i = 0; i < powdivmul.length; i++) {
    var p = powdivmul[i];
    var candidate = Math.pow(p[0], Math.floor(Math.log(range / (p[1] * p[2]))
                                              / Math.log(p[0]))) * p[2];
    candidates.push(candidate);
    candidates.push(candidate * p[0]);
  }
  var best_quality = 1e6;
  var best_step = candidates[0];
  var best_a0 = 0;
  for (var i = 0; i < candidates.length; i++) {
    var step = candidates[i];
    var a0 = Math.ceil(min / step) * step;
    var steps = Math.floor((max - a0) / step) + 1;
    if (steps < 2) continue;
    var quality = Math.abs(steps - 3.0) + Math.abs(steps * step / range) * 1.0;
    if (quality < best_quality) {
      best_quality = quality;
      best_step = step;
      best_a0 = a0;
    }
  }

  // Do the labelling.
  var a = best_a0;
  while (a <= max) {
    var x = datafunc(a);
    this.line(ctx, x, axis_y - 0, x, axis_y + 2);
    ctx.font = opts.small_text_height + 'px sans-serif';
    var msg = this.formatNumber(a);
    var textw = ctx.measureText(msg).width;
    var text_x = x - textw / 2;
    ctx.fillText(msg, text_x, axis_y + opts.small_text_height);
    a += best_step;
  }
};

// Return a two-element array, [min, max]
Plot.prototype.getDataMinMax = function(data) {
  if (data == undefined) {
    return [1e30, -1e30];
  } else if (typeof(data) == 'number') {
    return [data, data];
  } else if (typeof(data) == 'object' && data.type == 'box') {
    return [data.min, data.max];
  } else {
    return [1e30, -1e30];
  }
};

Plot.prototype.paintData = function(ctx, datafunc, width, y, data, color_index, opts) {
  var dh2 = opts.data_height / 2;
  var dh1 = opts.data_height - 1;
  var dot_radius = clamp(dh2, 1, 3);
  var box_size = clamp(dh1, 1, 5);
  y += dh2;  // Move down to center.
  var color = this.pickColor(color_index);
  if (data == undefined) {
    // Missing data: do a light gray dotted line.
    ctx.strokeStyle = '#EEE';
    this.line(ctx, opts.label_width, y, width, y);
    return;
  } else if (typeof(data) == 'number') {
    ctx.strokeStyle = color;
    var x = datafunc(data);
    ctx.strokeRect(x - box_size / 2, y - box_size / 2, box_size, box_size);
  } else if (typeof(data) == 'object' && data.type == 'box') {
    // 7-number summary: min, max, and 5 numbers q0..q4 defining the
    // percentiles 1, 25, 50, 75, 99.  Also has n == the
    // count of observations.
    //
    // Draw via a box plot.
    if (opts.small_text_height && data.n) {
      ctx.font = opts.small_text_height + 'px sans-serif';
      ctx.fillStyle = color;
      ctx.fillText('n=' + data.n, opts.label_width, y + dh2 - opts.small_text_height / 2 + 2);
    }
    var min = datafunc(data.min);
    var q0 = datafunc(data.q0);
    var q1 = datafunc(data.q1);
    var q2 = datafunc(data.q2);
    var q3 = datafunc(data.q3);
    var q4 = datafunc(data.q4);
    var max = datafunc(data.max);
    ctx.strokeStyle = color;
    this.line(ctx, q0, y - dh2, q0, y + dh2);
    this.line(ctx, q0, y, q1, y);
    ctx.strokeRect(q1, y - dh1 / 2, q3 - q1, dh1);
    // Draw a slightly fat line for the median marker.
    ctx.strokeRect(q2 - 0.5, y - dh1 / 2, 1.0, dh1);
    this.line(ctx, q3, y, q4, y);
    this.line(ctx, q4, y - dh2, q4, y + dh2);

    // min & max dots
    //ctx.fillStyle = '#F00';
    ctx.beginPath();
    ctx.arc(min, y, dot_radius, 0, 2 * Math.PI, false);
    ctx.stroke(); // ctx.fill();
    ctx.beginPath();
    ctx.arc(max, y, dot_radius, 0, 2 * Math.PI, false);
    ctx.stroke(); // ctx.fill();

  } else {
    // TODO
    log('paintMiniData unknown datatype');
  }
};

Plot.prototype.colors_ = ['#00F', '#880', '#808', '#088'];

Plot.prototype.pickColor = function(index) {
  return this.colors_[index % this.colors_.length];
};

// convenience
Plot.prototype.line = function(ctx, x0, y0, x1, y1) {
  ctx.beginPath();
  ctx.moveTo(x0, y0);
  ctx.lineTo(x1, y1);
  ctx.stroke();
};

Plot.prototype.fillOpts = function(opts, defaults) {
  if (!opts) { opts = {}; }
  opts.width = opts.width || defaults.width || 500;
  opts.row_pitch = opts.row_pitch || defaults.row_pitch || 5;
  opts.label_width = opts.label_width || defaults.label_width || 300;
  opts.text_height = opts.text_height || defaults.text_height || 10;
  opts.small_text_height = opts.small_text_height || defaults.small_text_height || 0;
  opts.label_axes = opts.label_axes || defaults.label_axes || false;
  opts.left_pad = opts.left_pad || defaults.left_pad || 25;
  opts.right_pad = opts.right_pad || defaults.right_pad || 5;

  opts.axis_label_height = opts.label_axes * opts.small_text_height;
  opts.data_height = Math.max(3, opts.row_pitch - 2);
  if (opts.row_pitch > 10) {
    opts.data_height = Math.max(3, Math.floor(opts.row_pitch * 0.8));
  }
  return opts;
};

// opts = {
//   width:
//   row_pitch:
//   label_width:
//   text_height:
//   small_text_height:
// };
Plot.prototype.appendPlot = function(elem, opts, small_opts) {
  var me = this;
  opts = me.fillOpts(opts, {
      width: 200,
      row_pitch: 30,
      label_width: 300,
      text_height: 14,
      small_text_height: 10,
      label_axes: true
    });
  small_opts = me.fillOpts(small_opts, {});

  map(me.groups_, function(k, group) {
      var heading = elem.ownerDocument.createElement('p');
      elem.appendChild(heading);
      var none_button = elem.ownerDocument.createElement('input');
      none_button.type = 'button';
      none_button.value = 'expand';
      none_button.style.display = 'none';
      var small_button = elem.ownerDocument.createElement('input');
      small_button.type = 'button';
      small_button.value = 'expand more';
      small_button.style.display = '';
      var large_button = elem.ownerDocument.createElement('input');
      large_button.type = 'button';
      large_button.value = 'collapse';
      large_button.style.display = 'none';
      heading.appendChild(none_button);
      heading.appendChild(small_button);
      heading.appendChild(large_button);
      var title = elem.ownerDocument.createElement('b');
      title.innerHTML = ' <b>' + html_escape(group) + '</b>';
      heading.appendChild(title);

      var small_plot = elem.ownerDocument.createElement('div');
      me.appendPlotSub(small_plot, small_opts, me.rows_by_group_[group]);
      small_plot.style.display = '';
      elem.appendChild(small_plot);
      //var small_plot = me.appendPlotSub(elem, small_opts, me.rows_by_group_[group]);
      //elem.appendChild(elem.ownerDocument.createElement('br'));

      var large_plot = elem.ownerDocument.createElement('div');
      me.appendPlotSub(large_plot, opts, me.rows_by_group_[group]);
      large_plot.style.display = 'none';
      elem.appendChild(large_plot);

      var cycle = function() {
        if (none_button.style.display == '') {
          none_button.style.display = 'none';
          small_button.style.display = '';
          small_plot.style.display = '';
        } else if (small_button.style.display == '') {
          small_button.style.display = 'none';
          small_plot.style.display = 'none';
          large_button.style.display = '';
          large_plot.style.display = '';
        } else {  // large button & plot visible
          large_button.style.display = 'none';
          large_plot.style.display = 'none';
          none_button.style.display = '';
        }
      };
      none_button.addEventListener('click', cycle, false);
      small_button.addEventListener('click', cycle, false);
      large_button.addEventListener('click', cycle, false);
    });
};

// rows = array of row names in this group.
Plot.prototype.appendPlotSub = function(elem, opts, rows) {
  var me = this;
  // Compute height.
  var sets = rows.length;
  var overlay_self = me.tags_.length > 1 && array_contains(me.tags_, 'self');
  var rows_per_set = me.tags_.length - overlay_self;
  var interstitial_rows = rows_per_set > 1 ? rows.length - 1 : 0;
  var axis_legends_height = rows * opts.small_text_height;
  var row_count = rows_per_set * sets + interstitial_rows;
  var height = row_count * opts.row_pitch + sets * opts.axis_label_height;

  opts.draw_tag_labels = /* rows_per_set > 1 && */ opts.text_height > 0;

  var canvas = elem.ownerDocument.createElement('canvas');
  canvas.width = opts.width;
  canvas.height = height;
  elem.appendChild(canvas)
  this.paint(canvas, opts, rows);
  return canvas;
};

// For interactive testing.
var Plot_selftest = function() {
  window.p = new Plot([
    {tag: 'bobo', metrics:{
     c0: {type: 'box', min: 1, q0: 1, q1: 3, q2: 3.5, q3: 4, q4: 5, max: 7, n: 300},
     c1: 10}},
    {tag: 'moses', metrics:{c0: 2, c1: 5, c3: 7}},
    {tag: 'momo', metrics: {
     c0: {type: 'box', min: -1, q0: 0, q1: 2, q2: 3, q3: 4, q4: 5, max: 6, n:12},
     c1: 5,
     c3: 3
      }},
    {tag: 'self', metrics: { c0: 3, c1: 4.1, c3: 7 }}
     ]);
  p.appendPlot(document.body, 200, 5, 0, 0, 0);
  p.appendPlot(document.body, 800, 25, 200, 12 /* text_height */, 10 /* small_text_height */);
};
